Understanding JSX
In the last lesson, we saw how to create a React element using plain, everyday JavaScript.
In reality, very few developers create elements this way. It's much more common to use a specialized syntax called JSX.
Here's that same example, but using JSX instead of JavaScript:
Code Playground
Instead of writing React.createElement
, we use an HTML-like syntax to create React elements.
Why do we use JSX? It might not be obvious from this tiny example, but as our chunk of markup grows, it becomes increasingly clear that JSX is just easier to read.
Remember how I mentioned that React elements can form a tree structure, just like HTML elements? This happens when we set the “children” parameter of a React element to another React element.
In practice, we often wind up with pretty significant tree structures in our React code. Here's an example, using plain JavaScript:
const element = React.createElement( "nav", { id: "main-nav" }, React.createElement( "ul", null, React.createElement( "li", null, React.createElement( "a", { href: "/" }, "Home" ) ), React.createElement( "li", null, React.createElement( "a", { href: "/archives" }, "Archives" ) ) ));
Pretty hard to read, right? Here's that same example in JSX:
// In JSX:const element = ( <nav id="main-nav"> <ul> <li> <a href="/"> Home </a> </li> <li> <a href="/archives"> Archives </a> </li> </ul> </nav>);
For whatever reason, HTML-like syntax is easier for our brains to process. It's nicer to read, and it's nicer to write.
Compiling JSX into JavaScript
If we try to run this JSX code in the browser, we'll get an error. JavaScript engines don't understand JSX, they only understand JavaScript. And so we need to "compile" our code into plain JS.
This is most commonly done as part of a build step, using a tool like Babel; we'll talk much more about this later in the course.
Here's the important thing to understand for now: The JSX we write gets converted into React.createElement
. By the time our code is running in the user's browser, all of the JSX has been zapped out, and we're left with a JS file full of nested React.createElement
calls.
Skipping the React import?
Let's look again at this code snippet:
import React from 'react';import { createRoot } from 'react-dom/client';
const element = ( <p id="hello"> Hello World! </p>);
const container = document.querySelector('#root');const root = createRoot(container);root.render(element);
On the very first line, we're importing React
, but we aren't actually using it anywhere… Are we? Can we omit it?
In fact, we are using the React import! Let's unpack what's going on here.
After we compile away the JSX, we're left with the following code:
import React from 'react';import { createRoot } from 'react-dom/client';
const element = React.createElement( 'p', { id: 'hello' }, 'Hello World!');
const container = document.querySelector('#root');const root = createRoot(container);root.render(element);
When the JSX is compiled into plain JS, the dependency makes itself clear. That <p>
tag becomes a React.createElement
call! It's by the JSX.
In earlier versions of React, you'd get an error if you forgot to include the React import:
Error: React is not defined
This error message produced a lot of confusion for beginners. Most tutorials gloss over how JSX actually works. And so, if you don't understand that <p>
becomes React.createElement('p')
, you won't have any idea what this error message is about, or how to fix it. 😬
This was such a common stumbling block for beginners that the React team decided to spend some time seeing how they could improve things. And they came up with a pretty clever solution!
With React 17, the React team introduced a new “JSX transformer”, used by Babel and other compilers. Essentially, it automatically injects the import during the build process.
For example, let's suppose we had this code:
const element = ( <p id="hello"> Hello World! </p>);
Using the modern JSX transformer, it will get compiled to:
import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx( 'p', { id: 'hello' }, 'Hello World!');
Note that our original code didn't include that import statement. It was included automatically by the compiler.
_jsx
is a fancy optimized version of React.createElement
. It includes some shortcuts when we use certain React features like Fragments or Portals. Otherwise, it does the exact same thing as React.createElement
: it creates a React element.
And so, these days, we don't have to import React. The JSX compiler will solve this problem for us.
Personally, though, I continue to import React whenever I work with JSX. It's partially that old habits are hard to break, and partially that we still do need to import React in order to use many React features, like hooks (covered in depth in Module 2 and Module 3). By always importing React, I know that it'll be there whenever I need it.